// 自定义shell的编写
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <unistd.h>
#include <errno.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unordered_map>
#include <iostream>
#include <ctype.h>
#include <sys/stat.h>
#include <fcntl.h>


// 重定向状态判断
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define ADD_REDIR 3
int redir = NONE_REDIR;
std::string filename;

// 全局别名映射表
std::unordered_map<std::string, std::string> alias_list;

// 为了能够记录上一个退出的进程的退出码，定义一个全局变量exitcode
int lastcode = 0;

// 维护一个全局命令行参数表和参数个数
#define MAX_ARGC 128
int g_argc = 0;
char *g_argv[MAX_ARGC];

// 命令行命令大小
#define COMMAND_SIZE 1024

// cwd当前工作路径
// cwdenv表示实时更新的pwd=xxx环境变量
char cwd[1024];
char cwdenv[1028];

// 命令行提示符的大小和格式
#define PROMPT_SIZE 1024
#define FORMAT "%s@%s:%s#"
// 全局的环境变量表
// 这里只是为了模仿初始化全局变量表的过程，实际上shell中并没有这个东西
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;

void InitEnv()
{
    // 这个是可变的每个进程独立拥有的全局变量表参数
    extern char **environ;
    // 先重置g_envs
    g_envs = 0;
    for (int i = 0; environ[i]; i++)
    {
        g_env[i] = environ[i];
        g_envs++;
    }
    // 因为从父进程获取的全局变量表和父进程的没区别，为了证明我们确实从父进程中读取并写入了自己的进程中，自己插入一个环境变量
    g_env[g_envs++] = (char *)"HAHA=for test";
    g_env[g_envs] = NULL; // C语言的字符串是以\0结尾
    // 我们定义的全局变量表相当于一个缓冲区，所以此时还需要将读取到的环境变量导出
    for (int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}
const char *GetUserName()
{
    const char *username = getenv("USER");
    return username == NULL ? "None" : username;
}
const char *GetHostName()
{
    char *hostname = (char *)calloc(128, sizeof(char));
    int ret = gethostname(hostname, 128);
    if (ret < 0)
    {
        printf("%s\n", hostname);
        perror("gethostname");
        exit(1);
    }
    return hostname == NULL ? "None" : (const char *)hostname;
}
const char *GetPwd()
{
    const char *pwd;
    // getenv获取到的pwd是当前进程维护的环境变量表中的内容,可能并不是当前工作路径，要获取正确的pwd应该使用
    // pwd接受返回值判断当前系统调用是否成功
    pwd = getcwd(cwd, sizeof(cwd));
    if (pwd != NULL)
    {
        // 调用成功
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    // 由于本次调用可能失败，所以在上面判断如果成功就更新我们维护的pwd环境变量
    return pwd == NULL ? "None" : pwd;
}
std::string DirName(const char *pwd)
{
    // 如果是root用户，家目录就是/root，如果是普通用户，家目录就是/home/USER
    std::string user = getenv("USER");
    std::string path = pwd;
    std::string pathtail;
    if (user == "root")
    {
        // root用户单独判断
        pathtail = path.substr(5);
    }
    else
    {
        // 普通用户:/home/user
        int size = user.size();
        pathtail = path.substr(6 + size);
    }
    std::string result = "~";
    result += pathtail;
    return result;
}
void MakePrompt(char prompt[], int size)
{
    // 怎么定义的就怎么传参,传的是数组就用数组接受
    // 用户名@主机名:~/一直到当前工作目录#（区分系统shell和自定义shell)
    snprintf(prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
void PrintCommandPrompt()
{
    // 定义一个缓冲区
    char prompt[PROMPT_SIZE];
    // 传入缓冲区（输出型参数）
    MakePrompt(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}
bool GetCommandLine(char line[], size_t size)
{
    // 从标准输入中读取用户输入的命令行参数
    // 一个输入示例：“ls -a -l\n"由于我们要整行读取，所以选择fgets
    char *ret = fgets(line, size, stdin);
    if (ret == NULL)
    {
        // 读取失败
        return false;
    }
    // 走到这里证明读取成功
    // 但是我们把\n也读取进来了
    line[strlen(line) - 1] = '\0';
    if (strlen(line) == 0)
        return false;
    return true;
}
// 由于strtok要修改传入参数,所以我们这次的输入型参数就不能用const了
bool ParserCommandLine(char *comline)
{
#define SEP " "
    // 我们维护了一个全局的命令行参数，所以这里只需要对它进行操作
    g_argc = 0;
    g_argv[g_argc++] = strtok(comline, SEP);
    while ((g_argv[g_argc++] = strtok(NULL, SEP)) != NULL)
        ;
    g_argc--;
    // true还是false取决于用户输入是否为空，为空就没必要进行下面的操作了
    return g_argc == 0 ? false : true;
}
void Execute()
{

    //"ls""-a""-l"
    // 参数已经存放在g_argv中了，接下来创建子进程通过程序替换的方式执行命令
    pid_t id = fork();
    if (id == 0)
    {
        int fd = -1;
        //判断是否存在重定向
        if(redir == INPUT_REDIR)
        {
            //int dup2(int oldfd,int newfd)
            //用旧的文件描述符的文件覆盖新的文件描述符指向的文件
            //失败返回-1
            //先打开一个新文件(filename)
            fd = open(filename.c_str(),O_RDONLY);
            //如果文件打开失败，子进程退出，重定向失败
            if(fd == -1) exit(1);
            //这个oldfd表示旧的文件描述符,那么哪个文件描述符是旧的呢
            dup2(fd,0);
            //表示关闭原来打开的文件描述符
            close(fd);
        }
        else if(redir == OUTPUT_REDIR)
        {
            //输出重定向
            //O_TRUNC表示是如果已经存在该文件，就截断为0,后面那个是设置权限掩码，只有在第一次创建该文件时有用
            fd = open(filename.c_str(),O_CREAT|O_RDONLY|O_TRUNC,0666);
            if(fd == -1) exit(1);
            dup2(fd,1);
            close(fd);
        }
        else if(redir == ADD_REDIR)
        {
            //追加重定向,如果不带上文件指针指向文件开始而不是末尾
            fd = open(filename.c_str(),O_CREAT|O_WRONLY|O_APPEND,0);
            if(fd == -1) exit(1);
            dup2(fd,1);
            close(fd);
        }


        // child
        // 命令，命令行参数
        execvp(g_argv[0], g_argv);
        exit(0);
    }
    // 由于父进程要更新最近退出进程返回值，所以等待时要获取状态status
    int status;
    pid_t rid = waitpid(id, &status, 0);
    // 如果等待失败，意味着子进程没有正常退出，那就是被异常信号杀死的，那么就没有退出码
    if (rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
}
// 这里定义bool类型，但我们不对它的返回值做判断
bool Cd()
{
    // cd aaa/bbb/ccc ..
    // 如果cd命令啥选项都没有，就是跳回家目录
    if (g_argc == 1)
    {
        std::string home = getenv("HOME");
        // 如果没有这个环境变量就啥也不做
        if (home.empty())
            return true;
        chdir(home.c_str());
    }
    else
    {
        std::string path = g_argv[1];
        if (path == "~")
        {
            // TODO
        }
        // 这是跳转到上次的目录
        else if (path == "-")
        {
            // TODO
        }
        else
        {
            chdir(path.c_str());
        }
    }
    return true;
}
void Echo()
{
    // echo "xxxxx"
    // echo $?
    if (g_argc == 2)
    {
        std::string opt = g_argv[1];
        if (opt == "$?")
        {
            std::cout << lastcode << std::endl;
            // 打印完要记得更新
            lastcode = 0;
        }
        else if (opt[0] == '$')
        {
            // 这个命令是要打印env环境变量
            std::string env_name = opt.substr(1);
            const char *env_val = getenv(env_name.c_str());
            if (env_val)
            {
                std::cout << env_val << std::endl;
            }
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}
bool CheckAndExecBuiltin()
{
    // 就判断命令行参数的命令呗，穷举
    std::string cmd = g_argv[0];
    if (cmd == "cd")
    {
        Cd();
        return true;
    }
    else if (cmd == "echo")
    {
        Echo();
        return true;
    }
    else if (cmd == "export")
    {
        // Export()
    }
    else if (cmd == "alias")
    {
        // 起别名，就用哈希映射，维护一张全局的映射表
        // std::string nickname = g_argv[1]
        // alias_list.pushback(k,v)
    }
    return false;
}
void SkipSpace(char comline[], size_t &start)
{
    while (isspace(comline[start]))
    {
        start++;
    }
}
void RedirCheck(char comline[])
{
    // ls -a -l >> test.txt   > < >>
    // 防止上次的状态影响
    redir = NONE_REDIR;
    filename.clear();
    // 重定向显然不止文件,这里是简化了并且默认了重定向符号后面是filename，并且默认只要有> < 等符号就是重定向
    size_t start = 0;
    size_t end = strlen(comline) - 1;

    while (start < end)
    {
        // 裁剪comline
        if (comline[start] == '<')
        {
            comline[start++] = '\0';
            // 这里执行让start跳过所有的空格直接指向第一个非空格字符
            SkipSpace(comline, start);
            // 此时start指向的是第一个非空字符
            redir = INPUT_REDIR;
            filename = comline + start;
            break;
        }
        else if (comline[start] == '>')
        {
            // 这里先判断是输出重定向还是追加
            if (comline[start + 1] == '>')
            {
                // 追加重定向
                comline[start] = '\0';
                start += 2;
                redir = ADD_REDIR;
            }
            else
            {
                // 输出重定向
                comline[start++] = '\0';
                redir = OUTPUT_REDIR;
            }
            SkipSpace(comline, start);
            filename = comline + start;
            break;
        }
        else
        {
            start++;
        }
    }
}
int main()
{
    // shell启动后，从系统的配置文件中获取环境变量
    // 而我们的shell从父shell中获取,并且我们要模仿父shell维护一张环境变量表
    // 初始化环境变量表
    InitEnv();
    while (true)
    {
        // 1.打印提示信息
        PrintCommandPrompt();
        // 2.获取命令行参数
        char commandline[COMMAND_SIZE];
        if (!GetCommandLine(commandline, sizeof(commandline)))
            continue;
        // 3.重定向分析
        RedirCheck(commandline);
        // 4.解析命令行参数
        if (!ParserCommandLine(commandline))
            continue;
        // 5.在执行命令前先判断一下该命令是否是内建命令，如果是内建命令，成功执行就返回true就continue
        if (CheckAndExecBuiltin())
            continue;

        // 6.执行命令
        Execute();
    }
    return 0;
}